package app

import (
	"context"
	"github.com/redis/go-redis/v9"
	"jgame-api/config"
	"log"
	"strconv"
	"sync"
	"time"
)

const unlock = "if redis.call('get', KEYS[1]) == ARGV[1] then redis.call('del', KEYS[1]); return 1; end return 0;"

type rdb interface {
	redis.Cmdable
	Do(ctx context.Context, args ...interface{}) *redis.Cmd
	Close() error
}

type RdbClient struct {
	mutex   sync.Mutex
	rdbType config.RdbType
	Client  rdb
}

func (r *RdbClient) Close() {
	_ = r.Client.Close()
}

// Lock 加锁
func (r *RdbClient) Lock(key, value string, expiration time.Duration) bool {
	r.mutex.Lock()
	defer r.mutex.Unlock()

	// 已经被其他goroutine加锁了
	if r.Client.Get(ctx, key).Val() != "" {
		return false
	}

	// 尝试设置Redis锁
	ok, err := r.Client.SetNX(ctx, key, value, expiration).Result()
	if err != nil {
		return false
	}
	return ok
}

// UnLock 解锁
func (r *RdbClient) Unlock(key, value string) bool {
	i, err := r.Client.Eval(ctx, unlock, []string{key}, value).Int()
	if err != nil {
		return false
	}
	return i > 0
}

// ZAdd ZAdd操作
func (r *RdbClient) ZAdd(key, member string, score int64) bool {
	cmd := r.Client.Do(ctx, "zadd", key, score, member)
	return cmd.Err() == nil
}

// ZRem ZRem操作
func (r *RdbClient) ZRem(key, member string) bool {
	cmd := r.Client.Do(ctx, "zrem", key, member)
	return cmd.Err() == nil
}

// ZIncrBy ZIncrBy操作
func (r *RdbClient) ZIncrBy(key, member string, delta int64) bool {
	cmd := r.Client.Do(ctx, "zincrby", key, delta, member)
	return cmd.Err() == nil
}

type ZsetMember struct {
	Member string
	Score  int64
}

// ZRevRangeWithScores操作
func (r *RdbClient) ZRevRangeWithScores(key string, start, stop int) []*ZsetMember {
	cmd := r.Client.Do(ctx, "zrevrange", key, start, stop, "withscores")
	if cmd.Err() != nil {
		return nil
	}

	val, ok := cmd.Val().([]interface{})
	if !ok {
		return nil
	}

	rs := make([]*ZsetMember, 0)

	var prev interface{}
	for i, v := range val {
		if i > 0 && i%2 == 1 {
			member := prev.(string)
			score := convertZsetScore(v)
			rs = append(rs, &ZsetMember{
				Member: member,
				Score:  score,
			})
			continue
		}
		prev = v
	}
	return rs
}

// ZRangeByScore操作
func (r *RdbClient) ZRangeByScore(key string, start, stop int64, offset, count int) []string {
	cmd := r.Client.Do(ctx, "zrangebyscore", key, start, stop, "LIMIT", offset, count)
	if cmd.Err() != nil {
		return nil
	}

	val, ok := cmd.Val().([]interface{})
	if !ok {
		return nil
	}

	rs := make([]string, 0)

	for _, v := range val {
		member := v.(string)
		rs = append(rs, member)
	}
	return rs
}

func (r *RdbClient) ZScore(key, member string) int64 {
	cmd := r.Client.Do(ctx, "zscore", key, member)
	if cmd.Err() != nil {
		return -1
	}
	score, err := cmd.Int64()
	if err != nil {
		return -1
	}
	return score
}

func convertZsetScore(v interface{}) int64 {
	switch v.(type) {
	// 创梦修改过redis server，返回的score类型是int64
	// 标准redis server，返回的score类型是string
	case int64:
		return v.(int64)
	case string:
		s := v.(string)
		r, e := strconv.ParseInt(s, 10, 64)
		if e != nil {
			return 0
		}
		return r
	default:
		return 0
	}
}

// createRedis 初始化Redis连接池
func createRedis(conf config.RedisConf) *RdbClient {
	timeout := 3 * time.Second
	if conf.Timeout > 0 {
		timeout = time.Duration(conf.Timeout) * time.Second
	}

	var rdb *RdbClient
	if conf.Type == config.RdbCluster {
		rdb = &RdbClient{Client: newClusterClient(conf, timeout), rdbType: conf.Type}
	} else if conf.Type == config.RdbSentinel {
		rdb = &RdbClient{Client: newFailoverClient(conf, timeout), rdbType: conf.Type}
	} else {
		rdb = &RdbClient{Client: newSingleClient(conf, timeout), rdbType: conf.Type}
	}

	if _, err := rdb.Client.Ping(ctx).Result(); err != nil {
		log.Fatalf("redis ping fail: %v", err)
	}
	return rdb
}

// newSingleClient 单机模式客户端
func newSingleClient(conf config.RedisConf, timeout time.Duration) *redis.Client {
	return redis.NewClient(&redis.Options{
		Addr:         conf.Addrs[0],
		Password:     conf.Password,
		PoolSize:     conf.PoolSize,
		MinIdleConns: conf.MaxIdleConn,
		ReadTimeout:  timeout,
		WriteTimeout: timeout,
	})
}

// newFailoverClient 哨兵模式客户端
func newFailoverClient(conf config.RedisConf, timeout time.Duration) *redis.Client {
	return redis.NewFailoverClient(&redis.FailoverOptions{
		MasterName:    conf.MasterName,
		SentinelAddrs: conf.Addrs,
		Password:      conf.Password,
		PoolSize:      conf.PoolSize,
		MinIdleConns:  conf.MaxIdleConn,
		ReadTimeout:   timeout,
		WriteTimeout:  timeout,
	})
}

// newClusterClient 集群模式客户端
func newClusterClient(conf config.RedisConf, timeout time.Duration) *redis.ClusterClient {
	return redis.NewClusterClient(&redis.ClusterOptions{
		Addrs:        conf.Addrs,
		Password:     conf.Password,
		PoolSize:     conf.PoolSize,
		MinIdleConns: conf.MaxIdleConn,
		ReadTimeout:  timeout,
		WriteTimeout: timeout,
	})
}
